package online.model.check;

import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;

import online.model.ModelUtil;
import online.model.UniModel;
import online.model.session.ActionParameter;
import online.model.session.SessionUser;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import core.config.Factory;
import core.exception.LogicalException;
import core.exception.PhysicalException;
import core.util.MapUtil;
import core.util.MojiUtil;
import core.util.bean.Pair;

/**
 * 入力チェッククラス
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class InputCheckImpl implements InputCheck {
	/** ログ出力オブジェクト */
	private static final Logger LOGGER = LogManager.getLogger(InputCheckImpl.class);

	/** トップメッセージ */
	private final List<String> message = new ArrayList<>();
	/** トップメッセージステータス */
	private final List<String> status = new ArrayList<>();
	/** 入力チェック保持リスト */
	private final List<Pair<ItemCheck, String[]>> chk = new ArrayList<>();
	/** 参照パラメタ */
	private final Map<String, String[]> ref = new HashMap<>();
	/** 変換チェック保持 */
	private final Map<String, ItemConvert<?>> conv = new HashMap<>();

	/** データモデル */
	private UniModel um = null;
	/** アクションパラメタ */
	private ActionParameter ap = null;
	/** セションユーザ情報 */
	private SessionUser su = null;
	/** エラー時操作 */
	private Consumer<LogicalException> ope = null;
	/** チェック対象判断ラムダ式 */
	private Predicate<Integer> pred = null;

	/**
	 * 汎用モデル設定
	 *
	 * @param model 汎用モデル
	 */
	@Override
	public void setUniModel(final UniModel model) {
		this.um = model;
	}

	/**
	 * アクションパラメタ設定
	 *
	 * @param param アクションパラメタ
	 */
	@Override
	public void setActionParameter(final ActionParameter param) {
		this.ap = param;
	}

	/**
	 * セションユーザ情報設定
	 *
	 * @param user セションユーザ情報
	 */
	@Override
	public void setSessionUser(final SessionUser user) {
		this.su = user;
	}

	/**
	 * チェック対象判断ラムダ式設定
	 *
	 * @param predicate チェック対象判断ラムダ式
	 */
	@Override
	public void setPredicate(final Predicate<Integer> predicate) {
		this.pred = predicate;
	}

	/**
	 * 入力チェック実行
	 *
	 * @param io チェッククラス
	 */
	@Override
	public void onError(final Consumer<LogicalException> io) {
		this.ope = io;
	}

	/**
	 * 入力チェック設定
	 *
	 * @param item 項目名
	 * @param ic チェッククラス
	 */
	@Override
	public void add(final String item, final ItemCheck... ic) {
		Stream.of(ic).forEach(c -> add(c, item));
	}

	/**
	 * 入力チェック設定
	 *
	 * @param ic チェッククラス
	 * @param items 項目名
	 */
	@Override
	public void add(final ItemCheck ic, final String... items) {
		setInputChecker(ic);

		this.chk.add(new Pair<>(ic, items));

		if (ItemConvert.class.isInstance(ic)) {
			for (final String itm : items) {
				if (this.conv.get(itm) == null) {
					this.conv.put(itm, ItemConvert.class.cast(ic));
				}
			}
		}
	}

	/**
	 * 入力チェッカー設定
	 *
	 * @param icb 入力チェック
	 */
	private void setInputChecker(final Object icb) {
		if (ItemCheckBase.class.isInstance(icb)) {
			ItemCheckBase.class.cast(icb).setActionParameter(this.ap);
			ItemCheckBase.class.cast(icb).setSessionUser(this.su);
			ItemCheckBase.class.cast(icb).setUniModel(this.um);
			ItemCheckBase.class.cast(icb).setReferenceMap(this.ref);
			ItemCheckBase.class.cast(icb).setMessageList(this.message, this.status);
			ItemCheckBase.class.cast(icb).setPredicate(this.pred);
		}
	}

	/**
	 * 入力チェックリスト削除
	 */
	@Override
	public void clear() {
		this.message.clear();
		this.status.clear();
		this.chk.clear();
		this.ref.clear();
		this.conv.clear();
	}

	/**
	 * チェック処理
	 */
	@Override
	public void check() {
		checkItem();
		clear();
	}

	/**
	 * チェック後入力値取込処理
	 */
	@Override
	public void populate() {
		checkItem();
		setAllRefParameter();
		clear();
	}

	/**
	 * チェック処理
	 */
	private void checkItem() {
		// チェックリスト数分処理
		Optional<Pair<ItemCheck, LogicalException>> error = Optional.empty();
		for (final Pair<ItemCheck, String[]> c : this.chk) {
			if (c != null && c.left() != null) {
				try {
					ItemCheck ic = c.left();
					ic.check(c.right());
				} catch (final NoMoreCheckException ex) {
					throwException(c.left(), ex);
				} catch (final KeepCheckException ex) {
					error = Optional.of(error.orElseGet(() -> new Pair<>(c.left(), ex)));
				} catch (final LogicalException | PhysicalException ex) {
					throw ex;
				}
			}
		}

		error.ifPresent(v -> throwException(v.left(), v.right()));
	}

	/**
	 * 参照したキーに対応する値をすべて設定する。
	 */
	private void setAllRefParameter() {
		for (final Entry<String, String[]> ent : this.ref.entrySet()) {
			ItemConvert<?> ic = MapUtil.get(ent.getKey(), this.conv, ItemConvert::identity);
			putTo(this.um, ent.getKey(), ic.convert(correct(ent.getValue())));
		}
	}

	/**
	 * 文字化け修正
	 *
	 * @param val 文字列配列
	 * @return 文字化け修正後配列
	 */
	private String[] correct(final String[] val) {
		Charset charset = ItemCheckBase.getMapping();
		if (!MojiUtil.isUtf(charset)) {
			return Stream.of(val).map(v -> MojiUtil.correctGarbled(v, charset)).
					toArray(String[]::new);
		}
		return val;
	}

	/**
	 * モデル設定
	 *
	 * @param <T> ジェネリックス
	 * @param model 汎用モデル
	 * @param key 設定キー
	 * @param val 設定値
	 */
	private static <T> void putTo(final UniModel model, final String key, final T[] val) {
		if (val.length == 1) {
			Method m = Factory.getMethod(model.getClass(), "setValue",
							String.class, val.getClass().getComponentType());
			Factory.invoke(model, m, key, val[0]);
		} else {
			Method m = Factory.getMethod(model.getClass(), "setValue",
							String.class, val.getClass());
			Factory.invoke(model, m, key, val);
		}
	}

	/**
	 * 例外処理
	 *
	 * @param check チェッククラス
	 * @param ex 例外
	 */
	private void throwException(final ItemCheck check, final LogicalException ex) {
		LOGGER.debug(check);

		try {
			if (this.ope != null) {
				setInputChecker(this.ope);
				this.ope.accept(ex);
			}
		} finally {
			for (int i = 0; i < this.message.size(); i++) {
				this.um.addValue(ModelUtil.TAG_MESSAGE, this.message.get(i));
				this.um.addValue(ModelUtil.TAG_STATUS, this.status.get(i));
			}
		}

		throw ex;
	}
}
